<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover"/>
<meta name="description" content="An example of virtualization where a very simple tree layout sets the bounds for each node data."/> 
<link rel="stylesheet" href="../assets/css/style.css"/> 
<!-- Copyright 1998-2021 by Northwoods Software Corporation. -->
<title>Virtualized Tree with custom layout</title>
</head>

<body>
  <!-- This top nav is not part of the sample code -->
  <nav id="navTop" class="w-full z-30 top-0 text-white bg-nwoods-primary">
    <div class="w-full container max-w-screen-lg mx-auto flex flex-wrap sm:flex-nowrap items-center justify-between mt-0 py-2">
      <div class="md:pl-4">
        <a class="text-white hover:text-white no-underline hover:no-underline
        font-bold text-2xl lg:text-4xl rounded-lg hover:bg-nwoods-secondary " href="../">
          <h1 class="mb-0 p-1 ">GoJS</h1>
        </a>
      </div>
      <button id="topnavButton" class="rounded-lg sm:hidden focus:outline-none focus:ring" aria-label="Navigation">
        <svg fill="currentColor" viewBox="0 0 20 20" class="w-6 h-6">
          <path id="topnavOpen" fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM9 15a1 1 0 011-1h6a1 1 0 110 2h-6a1 1 0 01-1-1z" clip-rule="evenodd"></path>
          <path id="topnavClosed" class="hidden" fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
        </svg>
      </button>
      <div id="topnavList" class="hidden lg:text-base sm:block items-center w-auto mt-0 text-white p-0 z-20">
        <ul class="list-reset list-none font-semibold flex justify-end flex-wrap sm:flex-nowrap items-center px-0 pb-0">
          <li class="p-1 sm:p-0"><a class="topnav-link" href="../learn/">Learn</a></li>
          <li class="p-1 sm:p-0"><a class="topnav-link" href="../samples/">Samples</a></li>
          <li class="p-1 sm:p-0"><a class="topnav-link" href="../intro/">Intro</a></li>
          <li class="p-1 sm:p-0"><a class="topnav-link" href="../api/">API</a></li>
          <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/products/register.html">Register</a></li>
          <li class="p-1 sm:p-0"><a class="topnav-link" href="../download.html">Download</a></li>
          <li class="p-1 sm:p-0"><a class="topnav-link" href="https://forum.nwoods.com/c/gojs/11">Forum</a></li>
          <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/contact.html"
           target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/contact.html', 'contact');">Contact</a></li>
          <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/sales/index.html"
           target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/sales/index.html', 'buy');">Buy</a></li>
        </ul>
      </div>
    </div>
    <hr class="border-b border-gray-600 opacity-50 my-0 py-0" />
  </nav>
  <div class="md:flex flex-col md:flex-row md:min-h-screen w-full max-w-screen-xl mx-auto">
    <div id="navSide" class="flex flex-col w-full md:w-48 text-gray-700 bg-white flex-shrink-0"></div>
    <!-- * * * * * * * * * * * * * -->
    <!-- Start of GoJS sample code -->
    
    <script src="../release/go.js"></script>
    <div class="p-4 w-full">
    <script id="code">
    // this controls whether the tree grows deeper towards the right or downwards:
    var HORIZONTAL = true;

    function init() {
      var $ = go.GraphObject.make;  // for conciseness in defining templates

      // The Diagram just shows what should be visible in the viewport.
      // Its model does NOT include node data for the whole graph, but only that
      // which might be visible in the viewport.
      myDiagram =
        $(go.Diagram, "myDiagramDiv",
          {
            initialDocumentSpot: go.Spot.Center,
            initialViewportSpot: go.Spot.Center,

            // Do manual layout in the layoutTree function below, rather than automatic layout using a
            // TreeLayout which would require the Nodes and Links to exist first for an accurate layout.
            //layout: $(go.TreeLayout,
            //          { nodeSpacing: 4, compaction: go.TreeLayout.CompactionNone }),
            // Assume there's no Layout -- all data.bounds are calculated in layoutTree
            layout: $(go.Layout, { isInitial: false, isOngoing: false }),  // never invalidates

            // Define the template for Nodes, used by virtualization.
            nodeTemplate:
              $(go.Node, "Auto",
                { isLayoutPositioned: false },  // optimization
                new go.Binding("position", "bounds", function(b) { return b.position; })
                  .makeTwoWay(function(p, d) { return new go.Rect(p.x, p.y, d.bounds.width, d.bounds.height); }),
                { width: 70, height: 20 },  // in cooperation with the load function, below
                $(go.Shape, "Rectangle",
                  new go.Binding("fill", "color")),
                $(go.TextBlock,
                  { margin: 2 },
                  new go.Binding("text", "color")),
                {
                  toolTip:
                    $("ToolTip",
                      $(go.TextBlock, { margin: 3 },
                        new go.Binding("text", "",
                          function(d) { return "key: " + d.key + "\nbounds: " + d.bounds.toString(); }))
                    )
                }
              ),

            // Define the template for Links
            linkTemplate:
              $(go.Link,
                {
                  isLayoutPositioned: false,  // optimization
                  fromSpot: (HORIZONTAL ? go.Spot.Right : go.Spot.Bottom),
                  toSpot: (HORIZONTAL ? go.Spot.Left : go.Spot.Top)
                },
                $(go.Shape)
              ),

            "animationManager.isEnabled": false
          });

      // This model includes all of the data
      myWholeModel =
        $(go.TreeModel);  // must match the TreeModel used by the Diagram, below

      // Do not set myDiagram.model = myWholeModel -- that would create a zillion Nodes and Links!
      // In the future Diagram may have built-in support for virtualization.
      // For now, we have to implement virtualization ourselves by having the Diagram's model
      // be different than the "real" model.
      myDiagram.model =  // this only holds nodes that should be in the viewport
        $(go.TreeModel); // must match the model, above

      // for now, we have to implement virtualization ourselves
      myDiagram.isVirtualized = true;
      myDiagram.addDiagramListener("ViewportBoundsChanged", onViewportChanged);

      // This is a status message
      myLoading =
        $(go.Part,  // this has to set the location or position explicitly
          { location: new go.Point(0, 0) },
          $(go.TextBlock, "loading...",
            { stroke: "red", font: "20pt sans-serif" }));

      // temporarily add the status indicator
      myDiagram.add(myLoading);

      // Allow the myLoading indicator to be shown now,
      // but allow objects added in load to also be considered part of the initial Diagram.
      // If you are not going to add temporary initial Parts, don't call delayInitialization.
      myDiagram.delayInitialization(load);
    }

    function load() {
      // create a lot of data for the myWholeModel
      var total = 123456;
      var treedata = [];
      for (var i = 0; i < total; i++) {
        var d = {
          key: i,  // this node data's key
          color: go.Brush.randomColor(),  // the node's color
          parent: (i > 0 ? Math.floor(Math.random() * i / 2) : undefined)  // the random parent's key
        };
        //!!!???@@@ this needs to be customized to account for your chosen Node template
        d.bounds = new go.Rect(0, 0, 70, 20);
        treedata.push(d);
      }
      myWholeModel.nodeDataArray = treedata;

      // make it faster to get from a model parent data object to all of the children data
      improveNavigation(myWholeModel);

      // this sets the data.bounds on each node data
      // and Diagram.fixedBounds on the diagram, so the diagram knows how far it can scroll
      layoutTree(myWholeModel);

      // remove the status indicator
      myDiagram.remove(myLoading);
    }

    // this adds ._parent and ._children properties on each node data
    function improveNavigation(model) {  // this must be a TreeModel
      var ndata = model.nodeDataArray;
      // create an Array of child data references for each parent data
      for (var i = 0; i < ndata.length; i++) {
        var child = ndata[i];
        var parentkey = model.getParentKeyForNodeData(child);
        var parent = model.findNodeDataForKey(parentkey);
        if (parent) {
          child._parent = parent;
          var childarr = parent._children;
          if (childarr) {
            childarr.push(child);
          } else {
            parent._children = [child];
          }
        }
      }
    }

    // The following functions implement virtualization of the Diagram
    // Assume data.bounds is a Rect of the area occupied by the Node in document coordinates.

    // The normal mechanism for determining the size of the document depends on all of the
    // Nodes and Links existing, so we need to use a function that depends only on the model data.
    function computeDocumentBounds() {
      var b = new go.Rect();
      var ndata = myWholeModel.nodeDataArray;
      for (var i = 0; i < ndata.length; i++) {
        var d = ndata[i];
        if (!d.bounds) continue;
        if (i === 0) {
          b.set(d.bounds);
        } else {
          b.unionRect(d.bounds);
        }
      }
      return b;
    }

    // As the user scrolls or zooms, make sure the Parts (Nodes and Links) exist in the viewport.
    function onViewportChanged(e) {
      var diagram = e.diagram;
      // make sure there are Nodes for each node data that is in the viewport
      // or that is connected to such a Node
      var viewb = diagram.viewportBounds;  // the new viewportBounds
      var model = diagram.model;

      var oldskips = diagram.skipsUndoManager;
      diagram.skipsUndoManager = true;

      var b = new go.Rect();
      var ndata = myWholeModel.nodeDataArray;
      for (var i = 0; i < ndata.length; i++) {
        var n = ndata[i];
        if (model.containsNodeData(n)) continue;
        if (!n.bounds) continue;
        if (n.bounds.intersectsRect(viewb)) {
          model.addNodeData(n);
        }
        // make sure links to all parent nodes appear
        var parentkey = myWholeModel.getParentKeyForNodeData(n);
        var parent = myWholeModel.findNodeDataForKey(parentkey);
        if (parent !== null) {
          if (n.bounds.intersectsRect(viewb)) {  // N is inside viewport
            model.addNodeData(parent);  // so that link to parent appears
          } else {  // N is outside of viewport
            // see if there's a parent that is in the viewport,
            // or if the link might cross over the viewport
            b.set(n.bounds);
            b.unionRect(parent.bounds);
            if (b.intersectsRect(viewb)) {
              model.addNodeData(n);  // add N so that link to parent appears
            }
          }
        }
      }

      diagram.skipsUndoManager = oldskips;

      if (myRemoveTimer === null) {
        // only remove offscreen nodes after a delay
        myRemoveTimer = setTimeout(function() { removeOffscreen(diagram); }, 3000);
      }

      updateCounts();  // only for this sample
    }

    // occasionally remove Parts that are offscreen from the Diagram
    var myRemoveTimer = null;

    function removeOffscreen(diagram) {
      myRemoveTimer = null;

      var viewb = diagram.viewportBounds;
      var model = diagram.model;
      var remove = [];  // collect for later removal
      var it = diagram.nodes;
      while (it.next()) {
        var n = it.value;
        var d = n.data;
        if (d === null) continue;
        if (!n.actualBounds.intersectsRect(viewb) && !n.isSelected) {
          // even if the node is out of the viewport, keep it if it is selected or
          // if any link connecting with the node is still in the viewport
          if (!n.linksConnected.any(function(l) { return l.actualBounds.intersectsRect(viewb); })) {
            remove.push(d);
          }
        }
      }

      if (remove.length > 0) {
        var oldskips = diagram.skipsUndoManager;
        diagram.skipsUndoManager = true;
        model.removeNodeDataCollection(remove);
        diagram.skipsUndoManager = oldskips;
      }

      updateCounts();  // only for this sample
    }
    // end of virtualized Diagram


    // A very simple tree layout.
    // Basic tree layout parameters
    var nodeSpacing = 4;
    var layerSpacing = 50;

    // Layout the whole tree just using the model, not any Nodes or Links.
    function layoutTree(model) {
      var ndata = model.nodeDataArray;
      // layout each tree root
      if (HORIZONTAL) {
        var y = 0;
        for (var i = 0; i < ndata.length; i++) {
          var d = ndata[i];
          // is this a root node?
          if (!d._parent) {
            y = walkTreeH(d, 0, y) + d.bounds.height + nodeSpacing;
          }
        }
      } else {  // !HORIZONTAL
        var x = 0;
        for (var i = 0; i < ndata.length; i++) {
          var d = ndata[i];
          // is this a root node?
          if (!d._parent) {
            x = walkTreeV(d, x, 0) + d.bounds.width + nodeSpacing;
          }
        }
      }

      // can't depend on regular bounds computation that depends on all Nodes existing
      myDiagram.fixedBounds = computeDocumentBounds();
    }

    // Walk subtrees from each root node, positioning as we go.
    function walkTreeH(parent, oldx, oldy) {  // HORIZONTAL
      var origy = oldy;
      var newy = oldy;
      var childarr = parent._children;
      if (childarr) {
        for (var i = 0; i < childarr.length; i++) {
          var child = childarr[i];
          newy = walkTreeH(child, oldx + child.bounds.width + layerSpacing, oldy);
          oldy = newy + child.bounds.height + nodeSpacing;
        }
      }
      parent.bounds.x = oldx;
      parent.bounds.y = (origy + newy) / 2;
      return newy;
    }

    function walkTreeV(parent, oldx, oldy) {  // !HORIZONTAL
      var origx = oldx;
      var newx = oldx;
      var childarr = parent._children;
      if (childarr) {
        for (var i = 0; i < childarr.length; i++) {
          var child = childarr[i];
          newx = walkTreeV(child, oldx, oldy + child.bounds.height + layerSpacing);
          oldx = newx + child.bounds.width + nodeSpacing;
        }
      }
      parent.bounds.x = (origx + newx) / 2;
      parent.bounds.y = oldy;
      return newx;
    }
    // end of layoutTree functionality

    // This function is only used in this sample to demonstrate the effects of the virtualization.
    // In a real application you would delete this function and all calls to it.
    function updateCounts() {
      document.getElementById("myMessage1").textContent = myWholeModel.nodeDataArray.length;
      document.getElementById("myMessage2").textContent = myDiagram.nodes.count;
      document.getElementById("myMessage4").textContent = myDiagram.links.count;
    }
    window.addEventListener('DOMContentLoaded', init);
  </script>

<div id="sample">
  <div id="myDiagramDiv" style="background-color: white; border: solid 1px black; width: 100%; height: 600px"></div>
  <div id="description">
  <p>This uses a <a>TreeModel</a> but not <a>TreeLayout</a>.</p>
  Node data in Model: <span id="myMessage1"></span>.
  Actual Nodes in Diagram: <span id="myMessage2"></span>.
  Actual Links in Diagram: <span id="myMessage4"></span>.
  </div>
</div>
    </div>
    <!-- * * * * * * * * * * * * * -->
    <!--  End of GoJS sample code  -->
  </div>
</body>
<!--  This script is part of the gojs.net website, and is not needed to run the sample -->
<script src="../assets/js/goSamples.js"></script>
</html>
